数据包处理过程

链路层判断收到的数据包类型,提取数据包中的数据字段,记录主机物理地址信息;IP层根据数据包中的IP地址实现数据的存储和转发,根据数据包编号实现数据包的重装,提取数据包中关于传输层的信息,向上层递交数据包并记录递交结果;TCP使用数据包中的信息更新TCP状态机并向应用程序递交数据。

数据包管理

在协议栈内核中移动的数据包有很多,比如:首先从网卡上接收的原始数据包,它可以是包含TCP报文的长达数百字节的数据包,也可以是仅有几十字节的ARP数据包;对于要发送的数据包,上层应用可能将各种各样的数据递交给LwIP内核发送,这些数据可能存在于应用进程管理的内存空间内,也可能存在于外部flash中。

数据在各层之间传递时,LwIP极力避免数据的拷贝工作,因为这样会耗费大量的时间和内存。

  • 数据包结构pbuf

    数据包管理机构采用数据结构pbuf来描述协议栈中使用的数据包:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    struct pbuf {
    struct pbuf* next; // 构成pbuf链表时指向下一个pbuf结构
    void* payload; // 数据指针,指向pbuf所激励的数据领域
    u16_t tot_len; // 当前pbuf及其后续所有pbuf中包含的数据总长度
    u16_t len; // 当前pbuf的数据的长度
    u8_t type; // 当前pbuf的类型
    u8_t flags; // 状态位
    u16_t ref; // 指向该pbuf的指针数,即该pbuf被引用的次数
    };
    • next:指向下一个pbuf结构,因为实际发送或接受的数据包可能很大,而每个pbuf能够管理的数据可能会有限,所以存在需要多个pbuf结构才能完全描述一个数据包的情况。

    • payload:数据指针,指向该pbuf管理的数据起始地址,这里数据起始地址可以是紧跟在pbuf结构之后的RAM空间中,也可能是ROM中的某个地址中,而决定这点的是当前pbuf的类型,即type字段的值。

    • len:当前pbuf中的有效数据长度,而tot_len表示当前pbuf和其后所有pbuf的有效数据的总长度,显然tot_len是len字段和pbuf链表中下一个pbuf的tot_len字段之和,pbuf链表中第一个pbuf的tot_len字段表示整个数据包的长度,而最后一个pbuf的tot_len字段必然同len字段相等。

    • type:表示pbuf的类型。

    • flags:通常设为0。

    • ref:pbuf被引用的次数,引用表示有其他指针指向当前buf,这里的指针可以是其他pbuf的next指针,也可以是其他形式的指针,初始化一个pbuf的时候,ref字段值被设置为1(因为该pbuf的地址一定会被返回给一个指针变量),当有其他指针指向该pbuf时,必须调用相关函数将pbuf的ref字段值增加。

    • pbuf的类型

      有4中类型,分别是PBUF_RAM,PBUF_ROM,PBUF_REF和PBUF_POOL:

      1
      2
      3
      4
      5
      6
      typedef enum {
      PBUF_RAM, // pbuf描述的数据在pbuf结构之后的连续内存堆中
      PBUF_ROM, // pbuf描述的数据在ROM中
      PBUF_REF, // pbuf描述的数据在RAM中,但位置与pbuf结构所处位置无关
      PBUF_POOL // pbuf结构与其描述的数据处于同一内存池中
      }pbuf_type;

      这4种类型中,PBUF_RAM类型的pbuf空间是通过内存堆分配得到的,使用最多,协议栈的待发送数据和应用程序的待发送数据一般都采用这个形式,申请PBUF_RAM类型的pbuf时,协议栈会在内存堆中分配相应空间,这里的大小包括如前所述的pbuf结构大小和相应缓冲区大小,它们在一片连续的内存堆存储区中。

      1
      p = (struct pbuf*)mem_malloc(LWIP_MEM_ALIGN_SIZE(SIZEOF_STRUCT_PBUF + offset) + LWIP_MEM_ALIGN_SIZE(length));

      通过这种方式分配的pbuf空间其pbuf结构和相应数据在一片连续的内存区域中,但是需要注意的是payload并没有指向整个数据区的起始处,而是间隔了一定区域,这段区域就是上面的offset,它用来存储数据包的各种首部字段,如TCP报文首部、IP首部、以太网帧首部等。

      PBUF_POOL类型和PBUF_RAM类型的pbuf很类似,但它的空间是通过内存池分配得到的,这种类型的pbuf可以在极短的时间内得到分配,在网卡接收数据包时,我们就使用了这种方式包装数据。在申请PBUF_POOL类型pbuf时,协议栈会在内存池MEMP_PBUF_POOL中选择一个或多个POOL,以满足用户空间大小的申请,源代码是通过下面一条语句来完成POOL申请的,如果用户发送的数据很长,那么系统会多次调用上面的语句,为用户分配多个POOL。

      1
      p = memp_malloc(MEMP_PBUF_POOL);
  • 数据包申请函数

    数据包申请函数pbuf_alloc在系统中许多地方会用到,例如在网卡接收数据时会申请一个数据包,然后将网卡中的数据填入数据包中;发送数据包时,协议栈的某层中会申请一个pbuf,并将相应的数据装入到数据区域,同时协议相关的首部信息也会被填到pbuf的预留数据区域中。

    数据包申请函数有两个重要的参数,一是想申请的数据包类型,这在上面已经有所介绍,而一个参数就是该数据包是在协议栈的哪一层被申请的,分配函数会根据层次的不同,在pbuf数据区域前为相关的协议预留出首部空间,也即offset。

    LwIP定义了四个层次,当数据包申请时,所处的层次不同,会导致预留空间的offset值不同,层次的定义是通过一个枚举类型pbuf_layer来实现的:

    1
    2
    3
    4
    5
    6
    7
    8
    #define PBUF_TRANSPORT_HLEN 20 // TCP报文首部长度
    #define PBUF_IP_HLEN 20 // IP数据报首部长度
    typedef enum {
    PBUF_TRANSPORT, // 传输层
    PBUF_IP, // 网络层
    PBUF_LINK, // 链路层
    PBUF_RAW // 原始层,不预留任何空间
    }pbuf_layer;

    pbuf_alloc函数的源码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    /*
    @layer: 指定该pbuf数据所处的层次,分配函数根据该值在pbuf数据区预留出首部空间 @length: 需要申请的数据区长度
    @type: pbuf的类型*/
    struct pbuf* pbuf_alloc(pbuf_layer layer, u16_t length, pbuf_type type) {
    struct pbuf *p, *q, *r;
    u16_t offset; // 预留首部空间的长度
    s32_t rem_len; //还需要申请的数据空间长度
    offset = 0; switch (layer) {  
    case PBUF_TRANSPORT:    
    offset += PBUF_TRANSPORT_HLEN; // 若在传输层,则预留出TCP首部大小  
    case PBUF_IP:    
    offset += PBUF_IP_HLEN; // 若在网络层或传输层,则还需留出IP首部大小  
    case PBUF_LINK: // 若在链路层或以上各层,则还需留出链路层首部    
    offset += PBUF_LINK_HLEN; // 以太网中,为以太网头部大小,即14字节    
    break;  
    case PBUF_RAW:    
    break;  
    default:     return NULL;
    }
    switch (type) {  
    case PBUF_POOL: // 最麻烦,可能需要分配多个POOL    
    p = memp_malloc(MEMP_PBUF_POOL);    
    if (p == NULL) {
    return NULL;    
    }    
    p->type = type;    
    p->next = NULL;    
    p->payload = LWIP_MEM_ALIGN((void*)((u8_t*)p + (SIZEOF_STRUCT_PBUF + offset)));    
    p->tot_len = length;    
    p->len = LWIP_MIN(length, PBUF_POOL_BUFSIZE_ALIGNED - LWIP_MEM_ALIGN_SIZE(offset));    
    p->ref = 1;    
    r = p;    
    rem_len = length - p->len;    
    while(rem_len > 0) {
    q = memp_alloc(MEMP_PBUF_POOL);      
    if (q == NULL) {
    pbuf_free(q);
    return NULL;
    }
    q->type = type;
    q->flags = 0;
    q->next = NULL;
    r->next = q;
    q->tot_len = (u16_t)rem_len;
    q_len = LWIP_MIN((u16_t)rem_len, PBUF_POOL_BUFSIZE_ALIGNED);
    q->payload = (void*)((u8_t*)q + SIZEOF_STRUCT_PBUF);
    q->ref = 1;
    rem_len = q->len;
    r = q;
    }
    break;
    case PBUF_RAM:
    p = (struct pbuf*)mem_alloc(LWIP_MEM_ALIGN_SIZE(SIZEOF_STRUCT_PBUF + offset) + LWIP_MEM_ALIGN_SIZE(length));
    if (p == NULL) {
    return NULL;
    }
    ...
    }
    }